# Copyright (c) 2009 The Chromium Embedded Framework Authors. All rights
# reserved. Use of this source code is governed by a BSD-style license that
# can be found in the LICENSE file.

from __future__ import absolute_import
from cef_parser import obj_header
from cef_version import VersionFormatter
from clang_util import clang_format
from file_util import *
import hashlib
from make_capi_header import write_capi_header
from make_capi_versions_header import write_capi_versions_header
from make_cpptoc_header import write_cpptoc_header
from make_cpptoc_impl import write_cpptoc_impl
from make_ctocpp_header import write_ctocpp_header
from make_ctocpp_impl import write_ctocpp_impl
from make_gypi_file import write_gypi_file
from make_libcef_dll_dylib_impl import write_libcef_dll_dylib_impl
from make_wrapper_types_header import write_wrapper_types_header
from optparse import OptionParser
import sys

FILE_HEADER = """#
# This file was generated by the CEF translator tool and should not edited
# by hand.
#
# $hash=$$HASH$$$
#

"""


def _write_version():
  return FILE_HEADER + VersionFormatter().get_version_string()


def _write_gitignore(gitignore, gitignore_file, root_dir):
  contents = FILE_HEADER

  in_file = gitignore_file + '.in'
  if os.path.isfile(in_file):
    contents += read_file(in_file)

  # Include ourselves in generated files.
  gitignore.append(gitignore_file)

  root_dir_len = len(root_dir)
  contents += '\n'.join(
      [p[root_dir_len:].replace('\\', '/') for p in sorted(gitignore)])

  return contents


def _update_file(file, newcontents, customized, force, clean, backup,
                 gitignore):
  """ Replaces the contents of |file| with |newcontents| if necessary. """
  if clean:
    if not customized:
      return 1 if remove_file(file, quiet=False) else 0
    print('File %s has customizations and will not be removed' % file)
    return 0

  if not customized and not gitignore is None:
    gitignore.append(file)

  oldcontents = ''
  oldhash = ''

  if newcontents[-1:] != "\n":
    # Add newline at end of file.
    newcontents += "\n"

  # clang-format is slow so we don't want to apply it if the pre-formatted
  # content hasn't changed. To check for changes we embed a hash of the pre-
  # formatted content in the resulting file.
  hash_start = "$hash="
  hash_end = "$"
  hash_token = "$$HASH$$"

  if not force and path_exists(file):
    oldcontents = read_file(file)

    # Extract the existing hash.
    start = oldcontents.find(hash_start)
    if start > 0:
      end = oldcontents.find(hash_end, start + len(hash_start))
      if end > 0:
        oldhash = oldcontents[start + len(hash_start):end]

  # Compute the new hash.
  newhash = hashlib.sha1(newcontents.encode('utf-8')).hexdigest()

  if oldhash == newhash:
    # Pre-formatted contents have not changed.
    return 0

  newcontents = newcontents.replace(hash_token, newhash, 1)

  # Apply clang-format for C/C++ files. This is slow, so we only do it for
  # customized files.
  if customized and os.path.splitext(file)[1][1:] in ('c', 'cc', 'cpp', 'h'):
    result = clang_format(file, newcontents)
    if result != None:
      newcontents = result
    else:
      raise Exception("Call to clang-format failed for %s" % file)

  if backup and oldcontents != '':
    backup_file(file)

  filedir = os.path.split(file)[0]
  if not os.path.isdir(filedir):
    make_dir(filedir)

  print('Writing file %s' % file)
  write_file(file, newcontents)
  return 1


def translate(cef_dir,
              force=False,
              clean=False,
              backup=False,
              verbose=False,
              selected_classes=None):
  # determine the paths
  root_dir = os.path.abspath(cef_dir)
  cpp_header_dir = os.path.join(root_dir, 'include')
  cpp_header_test_dir = os.path.join(cpp_header_dir, 'test')
  cpp_header_views_dir = os.path.join(cpp_header_dir, 'views')
  capi_header_dir = os.path.join(cpp_header_dir, 'capi')
  libcef_dll_dir = os.path.join(root_dir, 'libcef_dll')
  cpptoc_global_impl = os.path.join(libcef_dll_dir, 'libcef_dll.cc')
  ctocpp_global_impl = os.path.join(libcef_dll_dir, 'wrapper',
                                    'libcef_dll_wrapper.cc')
  wrapper_types_header = os.path.join(libcef_dll_dir, 'wrapper_types.h')
  cpptoc_dir = os.path.join(libcef_dll_dir, 'cpptoc')
  ctocpp_dir = os.path.join(libcef_dll_dir, 'ctocpp')
  gypi_file = os.path.join(root_dir, 'cef_paths.gypi')
  libcef_dll_dylib_impl = os.path.join(libcef_dll_dir, 'wrapper',
                                       'libcef_dll_dylib.cc')
  version_file = os.path.join(root_dir, 'VERSION.stamp')
  gitignore_file = os.path.join(root_dir, '.gitignore')

  # make sure the header directory exists
  if not path_exists(cpp_header_dir):
    sys.stderr.write('ERROR: Directory ' + cpp_header_dir +
                     ' does not exist.\n')
    sys.exit(1)

  # create the header object
  if verbose:
    print('Parsing C++ headers from ' + cpp_header_dir + '...')
  header = obj_header()

  # add include files to be processed
  header.set_root_directory(cpp_header_dir)
  excluded_files = [
      'cef_api_hash.h', 'cef_application_mac.h', 'cef_version_info.h'
  ]
  header.add_directory(cpp_header_dir, excluded_files)
  header.add_directory(cpp_header_test_dir)
  header.add_directory(cpp_header_views_dir)

  # Track the number of files that were written.
  writect = 0

  # Track files that are not customized.
  gitignore = []

  debug_string = ''

  try:

    # output the C API header
    if verbose:
      print('In C API header directory ' + capi_header_dir + '...')
    filenames = sorted(header.get_file_names())
    for filename in filenames:
      if verbose:
        print('Generating ' + filename + ' C API headers...')
      debug_string = 'CAPI header for ' + filename
      writect += _update_file(*write_capi_header(header, capi_header_dir,
                                                 filename), False, force, clean,
                              backup, gitignore)
      debug_string = 'CAPI versions header for ' + filename
      writect += _update_file(*write_capi_versions_header(
          header, capi_header_dir, filename), False, force, clean, backup,
                              gitignore)

    # output the wrapper types header
    if verbose:
      print('Generating wrapper types header...')
    debug_string = 'wrapper types header'
    writect += _update_file(*write_wrapper_types_header(
        header, wrapper_types_header), False, force, clean, backup, gitignore)

    # build the list of classes to parse
    allclasses = header.get_class_names()
    if not selected_classes is None:
      for cls in selected_classes:
        if not cls in allclasses:
          sys.stderr.write('ERROR: Unknown class: %s\n' % cls)
          sys.exit(1)
      classes = selected_classes
    else:
      classes = allclasses

    classes = sorted(classes)

    # output CppToC global file
    if verbose:
      print('Generating CppToC global implementation...')
    debug_string = 'CppToC global implementation'
    writect += _update_file(*write_cpptoc_impl(
        header, None, cpptoc_global_impl), force, clean, backup, gitignore)

    # output CToCpp global file
    if verbose:
      print('Generating CToCpp global implementation...')
    debug_string = 'CToCpp global implementation'
    writect += _update_file(*write_ctocpp_impl(
        header, None, ctocpp_global_impl), force, clean, backup, gitignore)

    # output CppToC class files
    if verbose:
      print('In CppToC directory ' + cpptoc_dir + '...')
    for cls in classes:
      if verbose:
        print('Generating ' + cls + 'CppToC class header...')
      debug_string = 'CppToC class header for ' + cls
      writect += _update_file(*write_cpptoc_header(header, cls, cpptoc_dir),
                              False, force, clean, backup, gitignore)

      if verbose:
        print('Generating ' + cls + 'CppToC class implementation...')
      debug_string = 'CppToC class implementation for ' + cls
      writect += _update_file(*write_cpptoc_impl(header, cls, cpptoc_dir),
                              force, clean, backup, gitignore)

    # output CppToC class files
    if verbose:
      print('In CToCpp directory ' + ctocpp_dir + '...')
    for cls in classes:
      if verbose:
        print('Generating ' + cls + 'CToCpp class header...')
      debug_string = 'CToCpp class header for ' + cls
      writect += _update_file(*write_ctocpp_header(header, cls, ctocpp_dir),
                              False, force, clean, backup, gitignore)
      if verbose:
        print('Generating ' + cls + 'CToCpp class implementation...')
      debug_string = 'CToCpp class implementation for ' + cls
      writect += _update_file(*write_ctocpp_impl(header, cls, ctocpp_dir),
                              force, clean, backup, gitignore)

    # output the gypi file
    if verbose:
      print('Generating ' + gypi_file + ' file...')
    debug_string = gypi_file
    writect += _update_file(*write_gypi_file(header, gypi_file), False, force,
                            clean, backup, gitignore)

    # output the libcef dll dylib file
    if verbose:
      print('Generating ' + libcef_dll_dylib_impl + ' file...')
    debug_string = libcef_dll_dylib_impl
    writect += _update_file(*write_libcef_dll_dylib_impl(
        header, libcef_dll_dylib_impl), False, force, clean, backup, gitignore)

    # output the VERSION.stamp file that triggers cef_version.h regen at build time
    if verbose:
      print('Generating ' + version_file + ' file...')
    debug_string = version_file
    writect += _update_file(version_file,
                            _write_version(), False, force, clean, backup,
                            gitignore)

    # output the top-level .gitignore file that lists uncustomized files
    if verbose:
      print('Generating ' + gitignore_file + ' file...')
    debug_string = gitignore_file
    writect += _update_file(gitignore_file,
                            _write_gitignore(gitignore, gitignore_file,
                                             root_dir), False, force, clean,
                            backup, None)

  except (AssertionError, Exception) as e:
    sys.stderr.write('ERROR: while processing %s\n' % debug_string)
    raise

  if verbose or writect > 0:
    print('Done translating - %s %d files.' % ('Removed'
                                               if clean else 'Wrote', writect))

  return writect


if __name__ == "__main__":
  from optparse import OptionParser

  # parse command-line options
  disc = """
  This utility generates files for the CEF C++ to C API translation layer.
  """

  parser = OptionParser(description=disc)
  parser.add_option(
      '--root-dir',
      dest='rootdir',
      metavar='DIR',
      help='CEF root directory [required]')
  parser.add_option(
      '--backup',
      action='store_true',
      dest='backup',
      default=False,
      help='create a backup of modified files')
  parser.add_option(
      '--force',
      action='store_true',
      dest='force',
      default=False,
      help='force rewrite of the file')
  parser.add_option(
      '--clean',
      action='store_true',
      dest='clean',
      default=False,
      help='clean all files without custom modifications')
  parser.add_option(
      '-c',
      '--classes',
      dest='classes',
      action='append',
      help='only translate the specified classes')
  parser.add_option(
      '-v',
      '--verbose',
      action='store_true',
      dest='verbose',
      default=False,
      help='output detailed status information')
  (options, args) = parser.parse_args()

  # the rootdir option is required
  if options.rootdir is None:
    parser.print_help(sys.stdout)
    sys.exit()

  if translate(options.rootdir, options.force, options.clean, options.backup,
               options.verbose, options.classes) == 0:
    if not options.verbose:
      print('Nothing to do.')
  elif not options.clean:
    print('WARNING: You must run version_manager.py to update API hashes.')
